#!/usr/bin/env node
//参考： https://elasticsearch.js.org/learn/developer
//参考： https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/api-reference.html
// 直接使用url访问：
// curl -X GET http://localhost:9200/_cat/indices?v #查看所有的表
// curl -XPUT --header 'Content-Type: application/json' 'http://localhost:9200/[index]/[type]/[_id]' -d '{"name":"tom","age":25}' #新增表
// curl -XPUT --header 'Content-Type: application/json' 'http://localhost:9200/test/_doc/1' -d' {"name":"tom","age":25}'
// curl -XPUT --header 'Content-Type: application/json' 'http://localhost:9200/[index]/[type]/[_id]/_update' -d '{"name":"tom","age":25}' #修改表
// curl -XGET http://localhost:9200/[index]/[type]/[_id]?pretty #根据id查询
// curl -XGET http://localhost:9200/[index]/[type]/_search?pretty #查询所有
// curl -XDELETE http://localhost:9200/[index]/[type]/[_id] #删除


const os = require('os');
const elasticsearch = require('elasticsearch');
const fs = require('fs-extra');
const path = require('path');
const moment = require('moment');
const command = require('./js/command');
const utils = require('./js/utils');
const _ = require('lodash');
const program = require('commander');
const shell = require('shelljs');
const configFile = path.join(os.homedir(), '.elasticsearch_config');
const CACHE = {};
let _host = 'localhost';
let _port = 9200;
let _username = '';
let _password = '';

let _verbose = true;
let _prettyShell = true;
let _highLightStyle;
let _limit = 1;
let _maxLimit = 1000;
async function createHBase(commands, options) {
    const db = new elasticsearch.Client({ host: `${_host}:${_port}`, httpAuth: _username && _password ? `${_username}:${_password}` : undefined, log: 'error' });
    const tables = await getTables(db);
    for (const table of tables) {
        await getFieldsOfTable(db, table);
    }
    // 添加表，字段的自动完成
    async function completer(line, callback) {
        const words = line.replace(/\s,\s/g, ',').trimLeft().split(/\s/);
        const linePartial = _.last(_.last(words).split(','));
        let list = [];
        if (words.length <= 1) {
            const cmds = ['scan', 'get', 'put', 'delete', 'deleteall'];
            const _linePartial = line.trim();
            list = cmds.filter((o) => o.startsWith(_linePartial));
            return callback(null, [list, _linePartial]);
        }

        const preWord = _.toLower(_.nth(words, -2));
        if (_.includes(['scan', 'get', 'put', 'delete', 'deleteall', 'ls', 's', 'm', 'r', 'c'], preWord)) {
            list = CACHE._tables.filter((o) => o.startsWith(linePartial));
        } else {
            const prePreWord = _.toLower(_.nth(words, -3));
            if (_.includes(['s', 'm', 'r', 'c'], prePreWord)) {
                const table = preWord;
                const fields = CACHE[table];
                list = fields.filter((o) => o.startsWith(linePartial));
            } else {
                const prePrePreWord = _.toLower(_.nth(words, -4));
                if (_.includes(['s', 'm', 'c'], prePrePreWord)) {
                    const table = prePreWord;
                    const fields = CACHE[table];
                    list = fields.filter((o) => o.startsWith(linePartial));
                } else {
                    let allFileds = [];
                    for (const table of CACHE._tables) {
                        const fields = CACHE[table];
                        allFileds = [...allFileds, ...fields];
                    }
                    allFileds = _.uniq(allFileds);
                    list = allFileds.filter((o) => o.startsWith(linePartial));
                }
            }
        }
        const found = list.filter((o) => o.startsWith(linePartial));
        return callback(null, [found.length ? found : list, linePartial]);
    }
    CACHE._options = { ...options, db, completer };
    command(commands, CACHE._options);
}
function base64(data) {
    return Buffer.from(data, 'utf8').toString('base64');
}
function dropBrack(line) {
    while (_.first(line) === '(' && _.last(line) === ')') {
        line = line.substr(1, line.length - 2);
    }
    return line;
}

function parseItem(item) {
    const list = item.match(/([\w\.]+)(!=|>=|>|<=|<|=|~|#)(.*)/);
    if (!list) {
        return null;
    }
    let key = list[1];
    let oper = list[2];
    let value = list[3];

    if (key === undefined || oper === undefined || value === undefined) {
        return null;
    }
    if (oper === '=') {
        return { match_phrase: { [key]: value } };
    }
    if (oper === '!=') {
        return { bool: { must_not: [{ match_phrase: { [key]: value } }] } };
    }
    return null;
}
function parseAnd(line) {
    if (!/\||&/.test(line)) {
        return parseItem(line);
    }
    const list = [];
    let i = -1, max = line.length - 1, item = '', match = 0;
    while (i++ < max) {
        const ch = line[i];
        if (ch === '(') {
            match++;
        } else if (ch === ')') {
            match--;
        }
        if (ch === '&' && match === 0) {
            list.push(parseAnd(dropBrack(item)));
            item = '';
        } else {
            item = `${item}${ch}`;
        }
    }
    if (match === 0) { // 如果括号不匹配，则丢弃
        if (line === item) {
            item && list.push(parseOr(dropBrack(item)));
        } else {
            item && list.push(parseAnd(dropBrack(item)));
        }
    }
    return list.length > 1 ? { bool: { must: list } } : list[0];
}
function parseOr(line) {
    if (!/\||&/.test(line)) {
        return parseItem(line);
    }
    const list = [];
    let i = -1, max = line.length - 1, item = '', match = 0;
    while (i++ < max) {
        const ch = line[i];
        if (ch === '(') {
            match++;
        } else if (ch === ')') {
            match--;
        }
        if (ch === '|' && match === 0) {
            list.push(parseOr(dropBrack(item)));
            item = '';
        } else {
            item = `${item}${ch}`;
        }
    }
    if (match === 0) { // 如果括号不匹配，则丢弃
        if (line === item) {
            item && list.push(parseAnd(dropBrack(item)));
        } else {
            item && list.push(parseOr(dropBrack(item)));
        }
    }
    return list.length > 1 ? { bool: { should: list } } : list[0];
}
function formatResult(obj) {
    if (obj instanceof Array) {
        return obj.map(o => formatResult(o));
    }
    for (const key in obj) {
        const item = obj[key];
        if (item instanceof Array) {
            obj[key] = formatResult(item);
        } else if (item instanceof Date) {
            obj[key] = moment(item).format('YYYY-MM-DD HH:mm:ss.SSS');
        }
    }
    return obj;
}
function formatQuery(condLine) {
    let cond;
    if (condLine) {
        if (/^[0-9a-z]{24}$/.test(condLine)) {
            condLine = `id=${condLine}`;
        } else if (/^1[0-9]{10}$/.test(condLine)) {
            condLine = `phone=${condLine}`;
        } else if (/^[0-9]{17}[0-9xX]$/.test(condLine)) {
            condLine = `idNo=${condLine}`;
        }
        cond = parseOr(dropBrack(condLine));
    }
    return cond;
}
function formatSetFields(line) {
    const list = [];
    let flag = true;
    while (flag) {
        let matches = line.match(/^([a-zA-Z][a-zA-Z0-9_]*):(.*)/);
        if (matches) {
            const key = matches[1];
            let value = matches[2];
            if (/^[^;]*=>{/.test(value)) { //带{函数
                let i = -1, max = value.length, match = 0;
                while (i++ < max) {
                    const ch = value[i];
                    if (ch === '{') {
                        match++;
                    } else if (ch === '}') {
                        match--;
                    }
                    if ((ch === ';' || !ch) && match === 0) {
                        line = value.substr(i + 1);
                        value = value.substr(0, i);
                        list.push({ key, value });
                        if (!line) {
                            flag = false;
                        }
                        break;
                    }
                }
                if (match !== 0) {
                    return null;
                }
            } else if (_.startsWith(value, '[')) { // 数组
                let i = -1, max = value.length, bmatch = 0, cmatch = 0;
                while (i++ < max) {
                    const ch = value[i];
                    if (ch === '[') {
                        bmatch++;
                    } else if (ch === ']') {
                        bmatch--;
                    } else if (ch === '{') {
                        cmatch++;
                    } else if (ch === '}') {
                        cmatch--;
                    }
                    if ((ch === ';' || !ch) && bmatch === 0) {
                        if (cmatch !== 0) {
                            return null;
                        }
                        line = value.substr(i + 1);
                        value = value.substr(0, i);
                        list.push({ key, value });
                        if (!line) {
                            flag = false;
                        }
                        break;
                    }
                }
                if (bmatch !== 0) {
                    return null;
                }
            } else if (_.startsWith(value, '{')) { // 对象
                let i = -1, max = value.length, bmatch = 0, cmatch = 0;
                while (i++ < max) {
                    const ch = value[i];
                    if (ch === '[') {
                        bmatch++;
                    } else if (ch === ']') {
                        bmatch--;
                    } else if (ch === '{') {
                        cmatch++;
                    } else if (ch === '}') {
                        cmatch--;
                    }
                    if ((ch === ';' || !ch) && cmatch === 0) {
                        if (bmatch !== 0) {
                            return null;
                        }
                        line = value.substr(i + 1);
                        value = value.substr(0, i);
                        list.push({ key, value });
                        if (!line) {
                            flag = false;
                        }
                        break;
                    }
                }
                if (cmatch !== 0) {
                    return null;
                }
            } else {
                matches = value.match(/([^;]*);(.*)/);
                if (!matches) {
                    list.push({ key, value });
                    break;
                }
                value = matches[1];
                list.push({ key, value });
                line = matches[2];
            }
        } else {
            return null;
        }
    }
    return list;
}
function formatUpdate(self, doc, setFields) {
    const set = {};
    for (const item of setFields) {
        const key = item.key;
        let value = item.value || '';

        if (value === 'true') {
            set[key] = true;
            continue;
        }
        if (value === 'false') {
            set[key] = false;
            continue;
        }
        if (value === 'null') {
            set[key] = null;
            continue;
        }
        const matches = _.uniq(value.match(/\$\w+/g));
        for (const match of matches) {
            const refValue = doc[match.replace('$', '')];
            if (refValue != undefined) {
                value = value.replace(new RegExp(match.replace('$', '\\$'), 'g'), refValue);
            }
        }
        // 解析函数
        if (/^[^;]*=>/.test(value)) {
            try {
                const func = (new Function(`return ${value}`))();
                set[key] = func(doc, { _, utils, moment });
            } catch (e) {
                self.error(`Function:${value} 格式错误`);
                return null;
            }
            continue;
        }
        // 解析数组
        if (/^\[.*]$/.test(value)) {
            try {
                set[key] = (new Function(`return ${value}`))();
            } catch (e) {
                self.error(`Array:${value} 格式错误`);
                return null;
            }
            continue;
        }
        // 解析obj
        if (/^{.*}$/.test(value)) {
            try {
                set[key] = (new Function(`return ${value}`))();
            } catch (e) {
                self.error(`Object:${value} 格式错误`);
                return null;
            }
            continue;
        }
        if (/^[0-9a-z]{24}$/.test(value)) {
            set[key] = mongojs.ObjectId(value);
            continue;
        }
        if (+value == value) {
            value = +value
        } else if (/\+|-|\*|\//.test(value)) {
            try {
                let _val = eval(value);
                if (typeof _val === 'number') {
                    value = _val;
                } else {
                    _val = value.replace(/^'(.*)'$/, '$1');
                    // 解析时间
                    if (/^\d{4}-\d{2}-\d{2}$/.test(_val)) {
                        set[key] = moment(_val, 'YYYY-MM-DD').toDate();
                        continue;
                    }
                    if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(_val)) {
                        set[key] = moment(_val, 'YYYY-MM-DD HH:mm:ss').toDate();
                        continue;
                    }
                    if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/.test(_val)) {
                        set[key] = moment(_val, 'YYYY-MM-DD HH:mm:ss.SSS').toDate();
                        continue;
                    }
                }
            } catch (err) { }
        }
        if (/^'.*'$|^".*"$/.test(value)) {
            value = value.replace(/^['"](.*)['"]$/, '$1');
        }
        set[key] = value;
    }
    return set;
}
async function formatFields(db, table, line) {
    if (!line || line === '*') {
        return null;
    }
    let include = true;
    if (line[0] === '-') {
        include = false;
        line = line.substr(1);
    }
    let list = line.split(',').filter(Boolean);
    const allFields = await getFieldsOfTable(db, table, true);
    list = list.map(o => new RegExp(o.replace('<', '^').replace('>', '$'), 'i'));

    const fields = allFields.filter(o => _.some(list, r => r.test(o)));
    return fields.length > 0 ? fields : null;
}
async function formatSort(db, table, line) {
    if (!line) {
        return [];
    }
    const sort = [];
    const allFields = await getFieldsOfTable(db, table);
    line = line.split(',');
    for (const i in line) {
        let item = line[i];
        let order = item[item.length - 1] === '+' ? 'asc' : 'desc';
        item = item.substr(0, item.length - 1);
        if (_.includes(allFields, item)) {
            sort.push({ [item]: { order } });
        }
    }
    return sort;
}
function formatLine(self, line) {
    let i = -1, max = line.length - 1, newLine = '', amatch = 0, bmatch = 0, cmatch = 0;
    while (i++ < max) {
        let ch = line[i];
        if (ch === '[') {
            amatch++;
        } else if (ch === ']') {
            amatch--;
        }
        if (ch === '{') {
            bmatch++;
        } else if (ch === '}') {
            bmatch--;
        }
        if (ch === '(') {
            cmatch++;
        } else if (ch === ')') {
            cmatch--;
        }
        if (amatch < 0 || bmatch < 0 || cmatch < 0) {
            self.error('括号不匹配');
            return [];
        }
        if (!(ch === ' ' && (amatch || bmatch || cmatch))) {
            newLine += ch;
        }
    }
    if (amatch || bmatch || cmatch) {
        self.error('括号不匹配');
        return [];
    }
    return newLine
        .replace(/"[^"]+"/g, o => o.replace(' ', '&nbsp;'))
        .replace(/'[^']+'/g, o => o.replace(' ', '&nbsp;'))
        .replace(/\s*:\s*/g, ':')
        .replace(/\s*;\s*/g, ';')
        .replace(/\s*,\s*/g, ',')
        .replace(/\s*!=\s*/g, '!=')
        .replace(/\s*!==\s*/g, '!==')
        .replace(/\s*=\s*/g, '=')
        .replace(/\s*==\s*/g, '==')
        .replace(/\s*~\s*/g, '~')
        .replace(/\s*\|\s*/g, '|')
        .replace(/\s*&\s*/g, '&')
        .replace(/\s*#\s*/g, '#')
        .replace(/\s*=>\s*/g, '=>')
        .trim().split(/\s/).filter(Boolean).map(o => o.replace('&nbsp;', ' '));
}
function j2s(obj) {
    return JSON.stringify(obj, (k, v) => {
        if (v instanceof RegExp) {
            return v.toString();
        }
        return v;
    });
}
async function getTableNames(db) {
    const list = await db.cat.indices({ format: "json" });
    return _.filter(_.map(list, 'index'), o => o[0] !== '.');
}
async function getTableNameAndInfo(db, line, short) {
    let list = await db.cat.indices({ format: "json" });
    line = line.substr(2).trim();
    list = _.filter(list, o => o.index[0] !== '.');
    if (line) {
        list = _.filter(list, o => o.index.includes(line));
    }
    if (short) {
        list = list.map(o => ({ index: o.index, count: o['docs.count'] }));
    }
    return list;
}
async function getFields(db, table) {
    const obj = await db.indices.getMapping({ index: table });
    if (_.get(obj[table], 'mappings.properties')) {
        return _.keys(_.get(obj[table], 'mappings.properties'));
    }
    const mappings = _.get(obj[table], 'mappings');
    let keys = [];
    for (const type in mappings) {
        keys = keys.concat(_.keys(mappings[type].properties));
    }
    return keys;
}
async function getTables(db, force) {
    if (!CACHE._tables || force) {
        CACHE._tables = await getTableNames(db);
    }
    return CACHE._tables;
}
async function getFieldsOfTable(db, table, force) {
    if (!CACHE[table] || force) {
        CACHE[table] = await getFields(db, table);
    }
    return CACHE[table];
}
async function executeLine(line, { db }) {
    if (!/^(scan|put|get|delete|deleteall)\s/.test(line)) {
        return this.prompt();
    }
    const list = line.split(/[\s,]/).filter(Boolean).map(o => o.replace('.', ':'));
    const cmd = `${list[0]} ${list.slice(1).map(o => `'${o}'`).join(',')}`;
    if (_verbose) {
        this.verbose(cmd);
    }
    shell.exec(`echo "${cmd}"|elasticsearch shell 2>&1|sed -n '/${list[0]}/,/in.*seconds/p'|sed '1d;$d'`, { silent: true }, (code, stdout, stderr) => {
        this.print(stdout);
        this.prompt();
    });
}
function showJson(self, docs) {
    self.showJson(formatResult(docs), _prettyShell, _highLightStyle);
}
async function showTables(line, { db }) {
    if (!line || line === '.') { // show tables
        this.print('tables:', 'red');
        const tables = await getTables(db, true);
        _verbose && this.print(`curl -X GET 'http://localhost:9200/_cat/indices?v&h=index'`);
        this.print(tables.join('  '), 'green');
        this.prompt();
    } else if (/-l.*/.test(line)) { // show tables detail
        _verbose && this.print(`curl -X GET http://localhost:9200/_cat/indices?v`);
        this.print('tables :', 'red');
        const tables = await getTableNameAndInfo(db, line);
        showJson(this, tables);
        this.prompt();
    } else if (/-s.*/.test(line)) { // show tables detail
        _verbose && this.print(`curl -X GET 'http://localhost:9200/_cat/indices?v&h=index,docs.count'`);
        this.print('tables :', 'red');
        const tables = await getTableNameAndInfo(db, line, true);
        showJson(this, tables);
        this.prompt();
    } else { // show table fields
        line = line.replace(/[^\w]*/g, '');
        const fields = await getFieldsOfTable(db, line, true);
        this.print(fields.join('  '), 'green');
        this.prompt();
    }
}
async function deleteTable(line, { db }) {
    let tables = await getTables(db, true);
    if (line !== '*') {
        const list = formatLine(this, line);
        tables = _.filter(tables, o => _.some(list, m => o.includes(m)));
    }
    if (!tables.length) {
        return this.error('no table match');
    }
    this.question(`ensure delete ${tables.join(',')} (y|n)`, async ret => {
        if (ret === 'y') {
            for (const index of tables) {
                await db.indices.delete({ index });
                _verbose && this.print(`curl -XDELETE http://localhost:9200/${index}`);
                this.print('delete done', 'green');
                this.prompt();
            }
        } else {
            this.print('delete canceled', 'green');
            this.prompt();
        }
    });
}
async function createTable(line, { db }) {
    let list = formatLine(this, line);
    let index, type, id, body = [];
    for (const item of list) {
        if (!/{/.test(item)) {
            if (body.length) {
                this.print(`insert table ${index}:`, 'green');
                showJson(this, body);
                try {
                    await db.bulk({ body, refresh: true });
                } catch (e) {
                    this.error(e.message);
                }
            }
            const arr = item.split(':');
            index = arr[0];
            type = arr[1] || '_doc';
            id = arr[2] || utils.uuid();
            body = [];
            try {
                await db.indices.create({ index });
            } catch (e) { }
            _verbose && this.print(`curl -XPUT --header 'Content-Type: application/json' 'http://localhost:9200/${index}/${type}/${id}'`);
        } else {
            if (index) {
                try {
                    const obj = (new Function(`return ${item}`))();
                    body.push({ create: { _index: index, _type: type, _id: id } });
                    body.push(obj);
                } catch (e) {
                    this.error(e.message);
                }
            }
        }
    }
    if (body.length) {
        _verbose && this.print(`curl -XPUT --header 'Content-Type: application/json' 'http://localhost:9200/${index}/${type}/${id}' -d '${JSON.stringify(body[1])}'`);
        _verbose && this.print('createTable:', 'blue');
        showJson(this, body);
        try {
            await db.bulk({ body, refresh: true });
        } catch (e) {
            this.error(e.message);
        }
    }
    this.prompt();
}
function getLimitIndex(list) {
    return _.findIndex(list, o => +o == o && o <= _maxLimit);
}
function getCondIndex(list) {
    return _.findIndex(list, o => /!=|>=|[^=]>[\w'"]|=[\w\u4e00-\u9fa5'"]|<=|<[\w'"]|~|#/.test(o) || /^[0-9a-z]{24}$/.test(o) || /^1[0-9]{10}$/.test(o) || /^[0-9]{17}[0-9xX]$/.test(o));
}
function getSortIndex(list) {
    return _.findIndex(list, o => /[a-zA-Z][a-zA-Z0-9_]*(\+|-)$/.test(o));
}
function getSetFieldsIndex(list) {
    return _.findIndex(list, o => /^[a-zA-Z][a-zA-Z0-9_]*:/.test(o));
}
function matchFields(line, column) {
    if (!line || line === '*') {
        return true;
    }
    let include = true;
    if (line[0] === '-') {
        include = false;
        line = line.substr(1);
    }
    let list = line.split(',').filter(Boolean);
    const names = column.split(':');
    let found = false;
    for (const item of list) {
        if (/\./.test(item)) {
            const items = item.split('.');
            if (items[0] === names[0]) {
                const reg = new RegExp(items[1].replace('<', '^').replace('>', '$'), 'i');
                if (reg.test(names[1])) {
                    found = true;
                }
            }
        } else {
            const reg = new RegExp(item.replace('<', '^').replace('>', '$'), 'i');
            if (reg.test(names[1])) {
                found = true;
            }
        }
    }
    return include ? found : !found;
}
async function showTableRows(line, { db }) {
    if (!line) {
        return this.prompt();
    }
    const matches = line.match(/out=[^\s]*/);
    let writeFile;
    if (matches) {
        writeFile = matches[0].replace('out=', '');
        line = line.replace(matches[0], '');
    }

    let showCount = false;
    let list = formatLine(this, line);
    const table = list[0];
    if (!table) {
        return this.error('invalid oper');
    }
    list = _.drop(list);

    let limit = 0;
    let limitIndex = getLimitIndex(list);
    if (limitIndex !== -1) {
        limit = +list[limitIndex];
        list.splice(limitIndex, 1);
        if (limit < 0) {
            showCount = true;
        }
    } else {
        limit = _limit;
    }

    let condLine = '';
    let condIndex = getCondIndex(list);
    if (condIndex !== -1) {
        condLine = list[condIndex];
        list.splice(condIndex, 1);
    }
    const query = formatQuery(condLine);

    let body = { query };
    let sort = '';
    let sortIndex = getSortIndex(list);
    if (sortIndex !== -1) {
        sort = await formatSort(db, table, list[sortIndex]);
        list.splice(sortIndex, 1);
        if (sort.length) {
            body.sort = sort;
        }
    }
    body = _.size(utils.omitNil(body)) ? body : undefined;
    if (showCount) {
        const option = utils.omitNil({
            index: table,
            body,
        });
        _verbose && this.print(`curl -XGET --header 'Content-Type: application/json' http://localhost:9200/${table}/_count?pretty -d '${JSON.stringify(_.omit(option, 'index'))}'`);
        const ret = await db.count(option);
        return showJson(this, { count: ret.count });
    }
    const fields = await formatFields(db, table, list[0]);
    const option = utils.omitNil({
        index: table,
        size: limit,
        from: 0,
        body,
        _source: fields,
    });
    _verbose && this.print(`curl -XGET --header 'Content-Type: application/json' http://localhost:9200/${table}/_search?pretty -d '${JSON.stringify(_.omit(option, 'index'))}'`);
    const ret = await db.search(option);
    const docs = _.map(_.get(ret, 'hits.hits'), o => _.assign(o._source, { _id: o._id, type: o._type }));
    if (writeFile) {
        fs.writeFileSync(writeFile, `module.exports=${JSON.stringify(formatResult(docs), null, 2)};`);
        this.print('write result to ' + writeFile);
        this.prompt();
    } else {
        showJson(this, docs);
    }
}
async function modifyTable(line, { db }) {
    if (!line) {
        return this.prompt();
    }
    let list = formatLine(this, line);
    const table = list[0];
    if (!table) {
        return this.error('invalid oper');
    }
    list = _.drop(list);

    let limit = 0;
    let limitIndex = getLimitIndex(list);
    if (limitIndex !== -1) {
        limit = +list[limitIndex];
        list.splice(limitIndex, 1);
    } else {
        limit = _limit;
    }

    let condLine = '';
    let condIndex = getCondIndex(list);
    if (condIndex !== -1) {
        condLine = list[condIndex];
        list.splice(condIndex, 1);
    }
    const query = formatQuery(condLine);
    let body = { query };

    let sort = '';
    let sortIndex = getSortIndex(list);
    if (sortIndex !== -1) {
        sort = formatSort(db, table, list[sortIndex]);
        list.splice(sortIndex, 1);
        if (sort.length) {
            body.sort = sort;
        }
    }

    let setFieldsIndex = getSetFieldsIndex(list);
    if (setFieldsIndex === -1) {
        return this.error('no set fields');
    }
    const setFields = formatSetFields(list[setFieldsIndex]);
    if (!setFields) {
        return this.error('set fields error');
    }
    list.splice(setFieldsIndex, 1);

    const option = utils.omitNil({
        index: table,
        size: limit,
        from: 0,
        body,
    });
    const ret = await db.search(option);
    const docs = _.map(_.get(ret, 'hits.hits'), o => _.assign(o._source, { _id: o._id, _type: o._type }));
    body = [];
    for (const doc of docs) {
        this.verbose(list[0]);
        const update = formatUpdate(this, doc, setFields);
        if (!update) {
            continue;
        }
        body.push({ update: { _index: table, _type: doc._type, _id: doc._id } });
        body.push({ doc: update });
        _verbose && this.print(`curl -XPUT --header 'Content-Type: application/json' http://localhost:9200/${table}/${doc._type}/${doc._id}/_update -d '${JSON.stringify(update)}'`);
    }
    if (!body.length) {
        this.print('no record', 'red');
        return this.prompt();
    }
    const r = await db.bulk({ body, refresh: true });
    if (r.error) {
        this.error(error);
    } else {
        showJson(this, r);
    }
}
async function removeTable(line, { db }) {
    if (!line) {
        return this.prompt();
    }
    let list = formatLine(this, line);
    const table = list[0];
    if (!table) {
        return this.error('invalid oper');
    }
    list = _.drop(list);

    let limit = 0;
    let limitIndex = getLimitIndex(list);
    if (limitIndex !== -1) {
        limit = +list[limitIndex];
        list.splice(limitIndex, 1);
    } else {
        limit = _limit;
    }

    let condLine = '';
    let condIndex = getCondIndex(list);
    if (condIndex !== -1) {
        condLine = list[condIndex];
        list.splice(condIndex, 1);
    }
    const query = formatQuery(condLine);
    let body = { query };

    let sort = '';
    let sortIndex = getSortIndex(list);
    if (sortIndex !== -1) {
        sort = formatSort(db, table, list[sortIndex]);
        list.splice(sortIndex, 1);
        if (sort.length) {
            body.sort = sort;
        }
    }

    const option = utils.omitNil({
        index: table,
        size: limit,
        from: 0,
        body,
    });
    if (_verbose) {
        this.verbose(`delete ${table}`);
    }
    const ret = await db.search(option);
    const docs = _.map(_.get(ret, 'hits.hits'), o => ({ _type: o._type, _id: o._id }));
    if (!docs.length) {
        return this.print('no record', 'red');
    }
    body = [];
    for (const doc of docs) {
        body.push({ delete: { _index: table, _type: doc._type, _id: doc._id } });
        _verbose && this.print(`curl -XDELETE http://localhost:9200/${table}/${doc._type}/${doc._id}`);
    }
    const r = await db.bulk({ body, refresh: true });
    if (r.error) {
        this.error(error);
    } else {
        showJson(this, r);
    }
}
async function copyTableRows(line, { db }) {
    if (!line) {
        return this.prompt();
    }
    let list = formatLine(this, line);
    const table = list[0];
    if (!table) {
        return this.error('invalid oper');
    }
    list = _.drop(list);

    let newIdFunc;
    let newIdIndex = _.findIndex(list, o => /newId=.*/.test(o));
    if (newIdIndex !== -1) {
        if (!/=>/.test(list[newIdIndex])) {
            newIdFunc = list[newIdIndex];
        } else {
            try {
                newIdFunc = (new Function(`return ${list[newIdIndex]}`))();
            } catch (e) {
                newIdFunc = (id) => `${id}_new`;
            }
        }
        list.splice(newIdIndex, 1);
    }

    let limit = 0;
    let limitIndex = getLimitIndex(list);
    if (limitIndex !== -1) {
        limit = +list[limitIndex];
        list.splice(limitIndex, 1);
    } else {
        limit = _limit;
    }

    let condLine = '';
    let condIndex = getCondIndex(list);
    if (condIndex !== -1) {
        condLine = list[condIndex];
        list.splice(condIndex, 1);
    }
    const query = formatQuery(condLine);
    let body = { query };

    let sort = '';
    let sortIndex = getSortIndex(list);
    if (sortIndex !== -1) {
        sort = formatSort(db, table, list[sortIndex]);
        list.splice(sortIndex, 1);
        if (sort.length) {
            body.sort = sort;
        }
    }

    let setFieldsIndex = getSetFieldsIndex(list);
    let setFields = null;
    if (setFieldsIndex !== -1) {
        setFields = list[setFieldsIndex];
        setFields = formatSetFields(setFields);
        if (!setFields) {
            return this.error('set fields error');
        }
        list.splice(setFieldsIndex, 1);
    }

    const option = utils.omitNil({
        index: table,
        size: limit,
        from: 0,
        body,
    });
    if (_verbose) {
        this.verbose(`copy ${table} ${JSON.stringify(option)}`);
    }

    const ret = await db.search(option);
    const docs = _.map(_.get(ret, 'hits.hits'), o => _.assign(o._source, { _id: o._id, type: o._type }));
    body = [];
    for (const doc of docs) {
        const { _id, type, ..._doc } = doc;
        const newId = newIdFunc && (_.isFunction(newIdFunc) ? newIdFunc(_id, doc) : newIdFunc);
        const update = setFields ? formatUpdate(this, _doc, setFields) : {};
        body.push({ create: { _index: table, _id: newId } });
        body.push({ ..._doc, ...update });
        if (_verbose) {
            this.verbose(`copy ${table} (${newId}, ${j2s(update)})`);
        }
    }
    if (!body.length) {
        return this.print('no record', 'red');
    }
    const r = await db.bulk({ body, refresh: true });
    if (r.error) {
        this.error(error);
    } else {
        showJson(this, r);
    }
}
function togglePrettyShell() {
    _prettyShell = !_prettyShell;
    this.print('pretty format change to ' + _prettyShell);
    this.prompt();
}
function toggleVerbose() {
    _verbose = !_verbose;
    this.print('verbose format change to ' + _verbose);
    this.prompt();
}
function setHighLightStyle(value) {
    _highLightStyle = value == 0 ? 'railscasts' : value == 1 ? 'github' : _highLightStyle == 'railscasts' ? 'github' : 'railscasts';
    this.print('highLightStyle set to ' + _highLightStyle);
    this.prompt();
}
function setMaxLimit(line) {
    _maxLimit = +line || 1000;
    this.print('set limit to ' + _maxLimit);
    this.prompt();
}
async function dumpDatabase(line = '') {
    let list = line.split(/\s+/).filter(Boolean);
    let index = list.indexOf('-o');
    let dir;
    if (index !== -1) {
        const items = list.splice(index, 2);
        dir = items[1];
    }
    if (!dir) {
        dir = _database;
    }
    if (fs.existsSync(path.join(dir, _database))) {
        return this.error(`${path.join(dir, _database)} 已经存在`);
    }
    let condition;
    if (_.find(list, o => /^-/.test(o))) {
        condition = _.map(list, o => `--excludeCollection ${o.replace(/^-/, '')}`).join(' ');
    } else {
        condition = _.map(list, o => `--collection ${o}`).join(' ');
    }

    const cmd = `elasticsearchdump -h ${_host} -d ${_database} ${condition} -o ${dir}`;
    this.log(`cmd: ${cmd}`);
    shell.exec(cmd, { silent: true }, (code, stdout, stderr) => {
        if (stderr && /error/.test(stderr)) {
            return this.error(stderr);
        }
        this.success(`目标文件：${process.cwd()}/${dir}`);
    });
}
function showHelp() {
    this.print('commands:', 'blue');
    this.print('    h: show help');
    this.print('    help: show full help');
    this.print('    <q|exit>: exit');
    this.print('    ls: showTables|ls table: showTableFields|ls -l|-s [table]: showTablesDetail');
    this.print('    s(showTableRows): s table f1,f2>,<f3> (f1=xx&f2=yy)|f3=zz|f4~<xx>|f5#1,2,3 limit(-1:count) sortField[+-] [out=file]');
    this.print('    m(modifyTable): m table f1:$f1+1;f2:xx;f3:(doc, _, moment)=>xx $cond limit sortField[+-]');
    this.print('    r(removeTable): # r table $cond limit sortField[+-]');
    this.print('    c(copyTableRows): c table $update $cond limit sortField[+-] newId=xx|(id, doc)=>xx');
    this.print('    delete(deleteTable): d table|*');
    this.print('    create(createTable): create table1[:type:id] [data1] table2[:type:id] [data2] ...');
    this.print('    format: [id: \"xx\"|\'xx\':string xx:ObjectId][date:\"xx\":string xx|\'xx\':date][number:\"xx\"|\'xx\':string xx:number]');
    this.print('    _: togglePrettyShell');
    this.print('    v: toggleVerbose');
    this.print('    hl: setHighLightStyle 0: railscasts 1: github else toggle');
    this.print('    limit: setMaxLimit');
    this.print('');
    this.prompt();
}
const COMMANDS = {
    'h': showHelp,
    'help': showHelp,
    'q|exit': function () { this.close() },
    'cls': function () { this.clear() },
    'ls': showTables,
    's': showTableRows,
    'm': modifyTable,
    'r': removeTable,
    'c': copyTableRows,
    'delete': deleteTable,
    'create': createTable,
    '_': togglePrettyShell,
    'v': toggleVerbose,
    'hl': setHighLightStyle,
    'limit': setMaxLimit,
    'default': executeLine,
};


program
    .version('1.0.0')
    .option('-t, --host [host]', 'set host', '')
    .option('-p, --port [port]', 'set port', '')
    .option('-u, --username [username]', 'set username', '')
    .option('-w, --password [password]', 'set password', '')
    .option('-c, --config', 'show config', '')
    .parse(process.argv);

let options = {};
try {
    options = fs.readJSONSync(configFile);
} catch (e) { }
_host = program.host || options.host || _host;
_port = program.port || options.port || _port;
_username = program.username || options.username || _username;
_password = program.password || options.password || _password;
if (options.host !== _host || options.port !== _port || options.username !== _username || options.password !== _password) {
    fs.writeJSONSync(configFile, { host: _host, port: _port, username: _username, password: _password });
}
if (program.config) {
    console.log({ host: _host, port: _port, username: _username, password: _password });
    process.exit(0);
}

createHBase(COMMANDS, { title: 'elasticsearch', history: ['ls', 's', 'm', 'r', 'c', 'create'] });
